package horde.comunications;

import horde.interfaces.MasterItf;
import horde.internalResources.HorrorControl.HorrorControl;
import horde.patterns.Msg;
import java.net.InetAddress;
import java.rmi.NotBoundException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

/**
 * This component manage all the comunications through RMI and offers the neccesary methods to do it.
 * @author Andres Picazo
 * @author Arturo Mazón
 */
public class RMIDock {

    // <editor-fold defaultstate="collapsed" desc="Atributes">
    private HashMap routingTable;
    /* Set of the diferent gateways */
    private HashSet<String> gateways = new HashSet<String>();
    private static RMIDock instance = null;
    private static final String id = "RMIDock";
    public static final String defaultMasterID = "Master";
    public static final int defaultRMIPort = 1099;
    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="Singleton">
    public static RMIDock getInstance() {
        if (instance == null) {
            createInstance();
        }
        return instance;
    }

    private static void createInstance() {
        if (instance == null) {
            instance = new RMIDock();
        }
    }

    private RMIDock() {
        routingTable = new HashMap();
    }

    /// </editor-fold>
    
    // <editor-fold defaultstate="collapsed" desc="Methods">
    /**
     * Checks if the given IP is well-formed
     * @param ip IP to check
     * @return True if the given IP is valid, false if not
     */
    private boolean validateIP(String ip) {
        String[] parts = ip.split("\\.");
        if (parts.length != 4) return false;
        else {
            for (String s : parts) {
                int i = Integer.parseInt(s);
                if ((i < 0) || (i > 255)) return false;
            }
            return true;
        }
    }

    /**
     * Converts a web site address to an IP address
     * @param route Web site name
     * @return IP address of the web site
     */
    private String addressToIp(String route) {
        try {
            InetAddress ip = InetAddress.getByName(route);
            return ip.getHostAddress();
        } catch (Exception e) {
            return "";
        }
    }

    /**
     * Adds a row to the routing table
     * @param agent Key of the row. Name of the agent.
     * @param node Name of the node destination
     * @param route IP address of destination
     * @return
     */
    public boolean addRoute(String agent, String node, String route) {
        if (!routingTable.containsKey(agent)) {
            if (validateIP(route)) {
                /* Add the route to the routing table */
                routingTable.put(agent, new routes(node, route));
                /* Add the gateway, if it's not already there */
                gateways.add(route);
            } else {
                /* Try dns query to get ip */
                try {
                    String ip = addressToIp(route);
                    if (validateIP(ip)) {
                        routingTable.put(agent, new routes(node, ip));
                        gateways.add(ip);
                    }
                    else return false;
                } catch (Exception e) {
                    return false;
                }                
            }
            trace(id, "Agent " + agent + " in Node " + node + " with path " + route + " added to routing table");
            return true;
        } else return false;
    }

    /**
     * Sends a given to message through RMI
     * @param m Message to be sent
     * @return True if sent, false if not sent or sent throught alternate route
     */
    public boolean post(Msg m) {
        routes r = (routes)routingTable.get(m.getDestination());
        MasterItf req;
        if (r != null) {
            try {
                req = getRemoteMasterRequests (r.path,defaultMasterID);
                if (req != null) {
                    trace (id, "Got requests for " + r.path);
                    req.post(m);
                    trace (id,"Remote message sent to " + m.getDestination());
                    return true;
                }
                else {
                    trace (id,"Remote master at " + r.path + " unavailable");
                    return false;
                }
            } catch (Exception e) {
                trace (id, "Exception thrown (post): " + e.getMessage());
                return false;
            }
        }
        else {
            /* Reroute the message throught all known gateways */
            for (String gate : gateways) {
                /* Hop list prevents multiple arrivals to same destination */
                if (!m.containsHop(gate)) {
                    try {
                        req = getRemoteMasterRequests (defaultMasterID, gate);
                        if (r != null) {
                            /* Adds this gate to the hop list */
                            m.addHop(gate);
                            req.post(m);
                            trace (id, "Message sent to " + m.getDestination() + " throught alternate gateway " + gate);
                        }
                        else trace (id, "Unable to send message throught alternate gate " + gate);
                    } catch (Exception e) {
                        trace (id, "Exception thrown: " + e.getMessage());
                        return false;
                    }
                }
            }
            return false;
        }
    }

    /**
     * Prints all the information of the routing table. The format is:
     * Key -> Gateway : ; Path : ;
     */
    public void showRoutingTable() {
        System.out.println("Size of routing table : " + routingTable.size());
        Set t = routingTable.keySet();
        for (Object o : t) {
            System.out.println(o.toString() + " -> " + routingTable.get(o).toString());
        }
    }

    public void trace (String src, String text, String date) {
        HorrorControl.getInstance().trace(src, text, HorrorControl.TRACE_INFO, date);
    }

    public void trace (String src, String text) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String date = sdf.format(Calendar.getInstance().getTime());
        HorrorControl.getInstance().trace(src, text, HorrorControl.TRACE_INFO, date);
    }

    public void traceRemote (String src, String text, String date) {
        HorrorControl.getInstance().trace(src, text, HorrorControl.TRACE_REMOTE, date);
    }

    public boolean startRMI () {
        try {
            LocateRegistry.createRegistry(defaultRMIPort);
            trace (id, "RMI registry created at port " + defaultRMIPort);
            return true;
        } catch (Exception e) {
            /* Checks if the registry already existed */
            try {
                Registry reg = LocateRegistry.getRegistry(defaultRMIPort);
                trace (id, "RMI registry already exists!!, registry ready at port " + defaultRMIPort);
                return true;
            } catch (Exception e2) {
                trace(id, "ERROR: RMI registry unavailable");
                return false;
            }
        }
    }

    public boolean registerObject (String id, Remote obj) {
        try {
            Registry reg = LocateRegistry.getRegistry(defaultRMIPort);
            reg.rebind(id, obj);
            trace (id, "Registered " + id + " at RMI \n(class:" + obj.getClass().getCanonicalName() + ")");
            trace (id, "Registered " + reg.list().length + " objects");
            return true;
        } catch (Exception e) {
            trace (id, "Unable to register object " + id + " at rmi registry.\n Cause: " + e.getMessage());
            return false;
        }
    }

    public boolean removeObject (String id) {
        try {
            LocateRegistry.getRegistry(defaultRMIPort).unbind(id);
            return true;
        } catch (Exception e) {
            trace (id, "Unable to remove object with id: " + id);
            return false;
        }
    }

    public void removeAllObjects () {
        try {
            Registry reg = LocateRegistry.getRegistry(defaultRMIPort);
            if (reg != null){
                String [] registeredObjects = reg.list();
                for (String s : registeredObjects) {
                    trace (id, "Unbinding " + s);
                    reg.unbind(s);
                }
            }
            
        } catch (Exception e) {
            trace (id, "Unable to unbind all objects");
        }
    }

    private MasterItf getRemoteMasterRequests (String ip, String id) {
        trace (id, "Locating master (" + id + ") at " + ip + "...");
        try {
            Registry reg = LocateRegistry.getRegistry(ip, defaultRMIPort);
            MasterItf r = (MasterItf)reg.lookup(id);
            return r;
        } catch (NotBoundException e) {
            trace (id, "Name " + id + " not bound to rmi registry located at " + ip + ":" + defaultRMIPort);
            return null;
        } catch (RemoteException e) {
            trace (id, "Connection failure at " + ip + ":" + defaultRMIPort + " \n" + e.getMessage());
            return null;
        } catch (Exception e) {
            trace (id, "Unknown exception thrown when trying to get master requests at " + ip + ":" + defaultRMIPort + " \n" + e.getMessage());
            return null;
        }
    }
    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="Class Routes">
    /**
     * Class that represents the value of each key of the routing table. It is a pair of values (gateway, path)
     */
    private class routes {
        // <editor-fold defaultstate="collapsed" desc="Atributes">

        String gateway;
        String path;

        // </editor-fold>
        // <editor-fold defaultstate="collapsed" desc="Builders">
        public routes() {
            gateway = "";
            path = "";
        }

        public routes(String g, String a) {
            gateway = g;
            path = a;
        }

        // </editor-fold>
        // <editor-fold defaultstate="collapsed" desc="Setters">
        public void setGateway(String g) {
            gateway = g;
        }

        public void setPath(String p) {
            path = p;
        }

        // </editor-fold>
        @Override
        public String toString() {
            return "Gateway : " + gateway + "; Path : " + path + " ;";
        }
    }
    // </editor-fold>
}
